/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2007 Ben Motmans <ben.motmans@gmail.com>
 * Copyright (C) 2009 Philippe Durand <draekz@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Collections.Generic;
using System.Reflection;

using Anculus.Core;
using Galaxium.Core;
using Galaxium.Client;
using Galaxium.Protocol;

namespace Galaxium.Protocol.Irc
{
	//http://en.wikipedia.org/wiki/Direct_Client-to-Client
	//http://www.irchelp.org/irchelp/rfc/ctcpspec.html
	public class DccFileConnection : TCPConnection
	{
		//TODO: allocate buffers ?
		
		private IrcFileTransfer _transfer;

		private bool _listenMode;
		private Thread _listenThread;
		private Socket _listenSocket;
		
		private BinaryWriter _writer;
		private FileStream _fileStream;

		public DccFileConnection (IrcSession session, IrcFileTransfer transfer, DccConnectionInfo connectionInfo)
			: base (session, connectionInfo)
		{
			this._transfer = transfer;
			this._listenMode = !connectionInfo.IsInbound;
		}

		public override void Connect ()
		{			
			if (_listenMode)
			{
				Log.Debug ("Listening for receiver to connect...");
				
				_intendedDisconnect = false;
				
				try
				{
					IPEndPoint listenEndPoint = null;
					_listenSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
					
					int tries = 5;
					bool success = false;
					
					// Attempt to open ports using upnp
					int externalPort = 0;
					NetworkUtility.CreatePortMap (_transfer.DccConnectionInfo.Port, out externalPort);
					
					while (--tries >= 0)
					{
						try
						{
							Log.Debug ("Trying to listen on port: "+_transfer.DccConnectionInfo.Port);
							
							listenEndPoint = new IPEndPoint (NetworkUtility.LocalIPAddress, _transfer.DccConnectionInfo.Port);
							_listenSocket.Bind (listenEndPoint);
							success = true;
							break;
						}
						catch (Exception e)
						{
							Log.Error (e, "Exception during trying");
						}
					}

					if (success)
					{
						Log.Debug ("Now listening on a seperate thread...");
						
						_listenSocket.Listen (1);
						
						_listenThread = new Thread (new ThreadStart (ListenSocketStart));
						_listenThread.Priority = ThreadPriority.Lowest;
						_listenThread.IsBackground = true;
						_listenThread.Start ();
					}
					else
					{
						Log.Debug ("Failed to setup a listening socket!");
						
						OnErrorOccurred (new ConnectionErrorEventArgs (this, "PROTOCOL_LISTEN", "Unable to listen for connections."));
					}
				}
				catch (SocketException ex)
				{
					Log.Error ("EndConnectCallback", ex);
					OnErrorOccurred (new ConnectionErrorEventArgs (this, "PROTOCOL_CONNECT", "Unable to connect to server."));
				}
			}
			else
			{
				Log.Debug ("Connecting to sender...");
				
				// Not sure if this is needed here... but it seemed to help during testing with other clients that dont use upnp
				int externalPort = 0;
				NetworkUtility.CreatePortMap (_transfer.DccConnectionInfo.Port, out externalPort);
				
				_fileStream = new FileStream (_transfer.FileName, FileMode.Create, FileAccess.Write);
				_writer = new BinaryWriter (_fileStream);
				
				base.Connect ();
			}
		}
		
		public override void Disconnect ()
		{
			if (_listenMode)
			{
				if (_listenThread != null)
				{
					try
					{
						_listenThread.Abort ();
					}
					catch (ThreadAbortException)
					{
						
					}
					finally
					{
						_listenThread = null;
					}
				}
				
				if (_listenSocket != null && _listenSocket.Connected)
				{
					_listenSocket.Disconnect (false);
					_listenSocket.Close ();
				}
			}
			else
			{
				base.Disconnect ();
			}
		}
		
		private void ListenSocketStart ()
		{
			try
			{
				_transfer.ChangeTransferState (TransferState.Pending);
				
				Log.Debug ("Attempting to accept the next connection.");
				
				Socket clientSocket = _listenSocket.Accept ();
				
				_transfer.ChangeTransferState (TransferState.Started);
				_transfer.SetTransferedBytes(0);
				
				//TODO: if reverse mode, we need to read the data that the client socket sends
				
				Log.Debug ("Connected, start sending the data...");
				
				using (_fileStream = File.OpenRead (_transfer.FileName))
				{
					using (BinaryReader reader = new BinaryReader (_fileStream))
					{
						_transfer.ChangeTransferState (TransferState.Progressing);
						
						int transfered = 0;
						bool eof = false;
						
						while (!eof)
						{
							Log.Debug ("Attempting to send 1024 bytes");
							// Read in 1024 bytes from the file.
							byte[] data = reader.ReadBytes (1024);
							int offset = 0;
							
							// If we have no data, get out.
							if (data == null || data.Length == 0)
							{
								eof = true;
								continue;
							}
							
							// Lets send those bytes
							while (offset < data.Length)
							{
								Log.Debug ("Sending> Offset: "+offset+" Length: "+data.Length);
								
								// Actually attempt to send as many bytes as we can.
								transfered += clientSocket.Send (data, offset, data.Length, SocketFlags.None);
								
								// Find out how many bytes were actually received so far
								byte[] bytesActual = new byte[4];
								int rec = clientSocket.Receive (bytesActual, 4, SocketFlags.None);
								Array.Reverse (bytesActual); // Reverse Little/Big
								int totalReceived = BitConverter.ToInt32(bytesActual, 0);
								Log.Debug ("Total Bytes Acknowledged: "+totalReceived);
								Log.Debug ("Total Bytes Sent: "+transfered);
								
								int discrepency = transfered - totalReceived;
								Log.Debug ("Discrepency: "+discrepency);
								Log.Debug ("Packet Length: "+data.Length);
								// Set the offset forward
								offset += data.Length - discrepency;
								Log.Debug ("Offset: "+offset);
								
								// Update how much the transfer has moved along.
								_transfer.SetTransferedBytes (totalReceived);
							}
						}
					}
				}
				
				_transfer.ChangeTransferState (TransferState.Finished);
				
				clientSocket.Disconnect (false);
				clientSocket.Close ();				
			}
			catch (SocketException ex)
			{
				_transfer.ChangeTransferState (TransferState.Failed);
				Log.Error (ex, "Exception during ListenSocketStart thread!");
				OnErrorOccurred (new ConnectionErrorEventArgs (this, "PROTOCOL_LISTEN", "Error while accepting socket."));
			}
		}
		
		protected override void Dispose (bool disposing)
		{
			if (!_disposed) {
				if (disposing) {
					if (_listenThread != null) {
						try {
							_listenThread.Abort ();
						} catch (ThreadAbortException) {
						} finally {
							_listenThread = null;
						}
					}
				}
			}

			base.Dispose (disposing);
		}
		
		protected override void OnDataReceived (ConnectionDataEventArgs args)
		{
			_transfer.ChangeTransferState (TransferState.Progressing);
			
			if (_listenMode)
			{
				// We do not use this event when listening!
			}
			else
			{
				// If we are receiving data that we are writing to file, lets
				// make sure we have the writer.
				if (_writer == null)
					return;
				
				_writer.Write (args.Buffer, 0, args.Length);
				
				_transfer.SetTransferedBytes (_transfer.TransferedBytes + args.Length);
				
				
				// Send ack of how much of the file we have now... make sure to reverse to big endian.
				Int32 number = (int)_transfer.TransferedBytes;
				byte[] bytes = System.BitConverter.GetBytes(number);
				if (BitConverter.IsLittleEndian)
					Array.Reverse(bytes);
				Send (bytes);
			}
		}
		
		protected override void OnClosed (ConnectionEventArgs args)
		{
			if (_transfer.TotalBytes <= 0)
			{
				_transfer.ChangeTransferState (TransferState.Finished);
			}
			else
			{
				if (_transfer.TransferedBytes == _transfer.TotalBytes)
					_transfer.ChangeTransferState (TransferState.Finished);
				else
					_transfer.ChangeTransferState (TransferState.Failed);
			}
			
			base.OnClosed (args);
		}

	}
}